/*
 * BLLib.Net.Connection
 * Andy Tidball
 * 
 * Project: Black Lightning General Library
 * Copyright: GNU General Public License
 */

using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;

namespace BLLib.Net {
	/// <summary>
	/// Simple abstraction of a socket which easily sends and receives strings.
	/// </summary>
	public class Connection {
		/// <summary>
		/// The size in bytes of a Connection's send buffer.
		/// </summary>
		public const int SendBufferSize = 1024;

		/// <summary>
		/// The size in bytes of a Connection's receive buffer.
		/// </summary>
		public const int ReceiveBufferSize = 1024;

		// member data
		private Socket         _Connection;         // the underlying socket connection
		private byte[]         _SendBuffer;         // a buffer to send data from
		private byte[]         _ReceiveBuffer;      // a buffer to receive data in
		private string         _SendQueue;          // a string waiting to be sent
		private Mutex          _SendQueueMutex;     // a mutex to control access to the send queue
		private AutoResetEvent _SendingData;        // signals between the asynchronous sending thread and the caller
		private int            _SendThreadsWaiting; // number of send threads waiting for the operation to finish

		/// <summary>
		/// Constructs a new Connection.
		/// </summary>
		public Connection() {
			// construct a new IPv4 TCP streaming socket
			_Connection = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

			// create our buffers
			_SendBuffer = new byte[SendBufferSize];
			_SendBuffer.Initialize();
			_ReceiveBuffer = new byte[ReceiveBufferSize];
			_ReceiveBuffer.Initialize();

			// create the send queue and its mutex
			_SendQueue          = "";
			_SendQueueMutex     = new Mutex();
			_SendingData        = new AutoResetEvent(false);
			_SendThreadsWaiting = 0;
		}

		/// <summary>
		/// Creates a new Connection which wraps around a given Socket.
		/// </summary>
		/// <param name="Socket">The Socket to encapsulate in the new Connection.</param>
		public Connection(Socket Socket) {
			// store the socket
			_Connection = Socket;

			// create our buffers
			_SendBuffer = new byte[SendBufferSize];
			_SendBuffer.Initialize();
			_ReceiveBuffer = new byte[ReceiveBufferSize];
			_ReceiveBuffer.Initialize();

			// create the send queue and its mutex
			_SendQueue          = "";
			_SendQueueMutex     = new Mutex();
			_SendingData        = new AutoResetEvent(false);
			_SendThreadsWaiting = 0;
		}

		/// <summary>
		/// True if the Connection is currently connected to the other end, false if not.
		/// </summary>
		public bool Connected {
			get {
				if (_Connection == null) {
					return false;
				} else {
					return _Connection.Connected;
				}
			}
		}

		/// <summary>
		/// Gets or sets whether most other calls to the Connection block by default.
		/// </summary>
		public bool Blocking {
			get {
				return _Connection.Blocking;
			}
			set {
				_Connection.Blocking = value;
			}
		}

		/// <summary>
		/// Gets the underlying socket for tweaking settings.
		/// </summary>
		public Socket Socket {
			get {
				return _Connection;
			}
		}

		/// <summary>
		/// Connects to a given domain name and port.
		/// </summary>
		/// <param name="Domain">The domain name to connect to.</param>
		/// <param name="Port">The remote port to connect to.</param>
		public void Connect(string Domain, int Port) {
			Connect(Dns.GetHostEntry(Domain).AddressList[0], Port);
		}

		/// <summary>
		/// Connects to a given IP address and port.
		/// </summary>
		/// <param name="IP">The IP address to connect to.</param>
		/// <param name="Port">The remote port to connect to.</param>
		public void Connect(IPAddress IP, int Port) {
			// if we're currently connected then we can't connect again or to somewhere else
			// we could theoretically let them connect somewhere else, but it'd be better for them to just make a new Connection
			if (Connected) {
				throw new InvalidOperationException("Cannot connect when the Connection is already connected.");
			}

			// connect to the requested address
			if (Blocking) {
				_Connection.Connect(IP, Port);
			} else {
				_Connection.BeginConnect(IP, Port, null, null);
			}
		}

		/// <summary>
		/// Disconnects the Connection from the other end and frees its resources
		/// </summary>
		public void Disconnect() {
			if (_Connection == null) {
				return;
			}

			// shutdown the socket
			try {
				_Connection.Shutdown(SocketShutdown.Both);
			} catch (SocketException) { }

			// close it to free its resources
			_Connection.Close();
			_Connection = null;
		}

		/// <summary>
		/// Sends the given string across the Connection to the other end.
		/// </summary>
		/// <param name="Data">The data to send across the Connection.</param>
		public void Send(string Data) {
			// if we're not connected, throw an exception
			if (!Connected) {
				throw new SocketException(10057);
			}

			// append the string to the SendQueue
			_SendQueueMutex.WaitOne();
			_SendQueue += Data;
			_SendQueueMutex.ReleaseMutex();

			// record that a thread will be waiting for the send data
			++_SendThreadsWaiting;

			// send the queued up data
			SendQueuedData();

			// if they want to block until we're done, then wait for it
			if (Blocking) {
				_SendingData.WaitOne();
			}

			// record that this thread has finished waiting
			--_SendThreadsWaiting;
		}

		/// <summary>
		/// Sends any data waiting in the SendQueue across the Connection.
		/// </summary>
		private void SendQueuedData() {
			// if there's nothing in the send queue, then we're finished
			if (_SendQueue == "") {
				// if there are send threads waiting for us to finish, signal that we're done sending
				if (_SendThreadsWaiting > 0) {
					_SendingData.Set();
				}

				return;
			}

			// grab as much data as we can from the send queue
			_SendQueueMutex.WaitOne();
			string DataToSend;
			if (_SendQueue.Length > _SendBuffer.Length) {
				DataToSend = _SendQueue.Substring(0, Connection.SendBufferSize);
				_SendQueue = _SendQueue.Substring(Connection.SendBufferSize);
			} else {
				DataToSend = _SendQueue;
				_SendQueue = "";
			}
			_SendQueueMutex.ReleaseMutex();

			// copy the data to the send buffer
			for (int i=0; i<DataToSend.Length; ++i) {
				_SendBuffer[i] = (byte)DataToSend[i];
			}

			// start an asynchronous send operation
			_Connection.BeginSend(_SendBuffer, 0, DataToSend.Length, SocketFlags.None, SendComplete, null);
		}

		/// <summary>
		/// Callback for when an asynchronous send is complete.
		/// </summary>
		/// <param name="Result">The result of the asynchronous call.</param>
		private void SendComplete(IAsyncResult Result) {
			// send the next set of queued data
			SendQueuedData();
		}

		/// <summary>
		/// Receives a string of data from across the Connection.
		/// </summary>
		/// <returns>The string received from across the Connection, or null if Blocking is false and none was available.  If this method was blocking and then returns an empty string, the other end of the Connection was disconnected.</returns>
		public string Receive() {
			// if we can't block and there's no data, return null now
			if (!Blocking && _Connection.Available == 0) {
				return null;
			}

			// receive data from the socket into the receive buffer
			int Received = 0;
			try {
				Received = _Connection.Receive(_ReceiveBuffer, Connection.ReceiveBufferSize, SocketFlags.None);
			} catch (SocketException x) {
				if (x.SocketErrorCode == SocketError.TimedOut) {
					return null;
				} else {
					throw;
				}
			}

			// build a string out of the receive buffer
			string StringReceived = Encoding.ASCII.GetString(_ReceiveBuffer, 0, Received);

			// blank out the buffer for next time
			_ReceiveBuffer.Initialize();

			// return the received string
			return StringReceived;
		}

		/// <summary>
		/// Sets the socket to listen for incoming connections
		/// </summary>
		/// <param name="Port">The local port to bind to and listen on.</param>
		/// <param name="QueueSize">The number of pending connections to accept before denying new ones.</param>
		public void Listen(int Port, int QueueSize) {
			// bind the socket to the given port
			_Connection.Bind(new IPEndPoint(IPAddress.Any, Port));

			// have the socket listen for connections
			_Connection.Listen(QueueSize);
		}

		/// <summary>
		/// Accepts a connection from elsewhere on a Connection which is Listening.
		/// </summary>
		/// <returns>A new Connection which represents the newly established connection, or null for a non-blocking call when no incoming connections are queued.</returns>
		public Connection Accept() {
			// accept a connection from the socket and return the new socket wrapped in a new Connection
			try {
				return new Connection(_Connection.Accept());
			} catch (SocketException x) {
				if (x.SocketErrorCode == SocketError.WouldBlock) {
					// we're not blocking and there's no connections to accept, return null
					return null;
				} else {
					throw;
				}
			}
		}
	}
}
